suspend他並不能切換線程,切換線程的是內部自帶的suspend函數,ex. withContext
coroutine只是能用阻塞寫法寫出非阻塞代碼,本質和thread是一樣的
剛接觸coroutine的人應該都會遇到這個名詞,掛起函數,蛤?
掛起?掛起誰,掛在哪裡,什麼時候掛起,讓我一個一個回答
我們前面講過launch/ async實際上做了甚麼,將任務post到thread裡面,對吧?
那今天如果任務是掛起的呢? 那就掛起呀,以範例來說,Main thread執行到suspendThis()的時候,就會結束我們post過去的任務,去做他原本該做的事
ㄟ等等,但我們的任務還沒做完呀?
不急不急,聽我接著講,任務結束,是對Main thread來說結束,而任務本身會透過withContext繼續在IO thread執行,還記得我們說過withContext的特性吧,暫時切換thread,然後再切回去,沒錯,任務結束後,withContext又會自動幫我們切回去,而這所謂的自動切回,就是coroutine會再幫我們post一個任務,讓我們回到原先的thread繼續執行

所謂的掛起函數,就是稍後會被自動切回來的thread切換。 by扔物線
val scope = CoroutineScope(rootJob)
        
scope.launch {
    suspendThis()
    Log.i("","")
}
//變成
handler.post{
   suspendThis()
   Log.i("","")
}
suspend fun suspendThis(){
    withContext ( Dispatchers.IO ){
        // io task
    }
}
隨便找了一張圖,但我不打算講thread,只是給你看剛剛講到的東西
圖源
那要suspend幹嗎? 他又不負責切線程,拿掉不行嗎? 诶~真不行,它的用處,現在才要開始
suspend是個coroutine很常見的關鍵字,幾乎到處都能看到他的身影,標記了suspend的方法,一定要在coroutine或另外的suspend內使用,當調用了supend方法,會暫停當前coroutine的執行,並保留所有局部變量,並在結束後resume,並執行之後的code
suspend — pause the execution of the current coroutine, saving all local variables
resume — continue a suspended coroutine from the place it was paused
在語法方面,suspend本身是提醒開發者,這項任務需要耗時,或是切換thread,請要coroutine裡面適當的調用我,而真正耗時的部分是suspend fun裡面的code
這個提醒,有用嗎?大有用處,我們自己都有可能忘記某個fun是耗時的,更不用說,如果你用了一個package,你也不知道他的代碼是耗時的呀,一不小心,ui卡一下,又要通靈抓bug了,那如果有提醒的話呢?ide會告訴方法的調用者,我是耗時任務,在coroutine裡面調用我
suspend方法並不會讓kotlin在後臺執行函數,在主線程使用suspend或是啟動協程是相當常見的,而我們應該使用withContext(),或其他方式確保主線程安全
前面講過callback沒有不見,而是編譯器透過finite state machine將suspend fun轉換為callback的版本
TL;DR; The Kotlin compiler will create a state machine for every suspend function that manages the coroutine’s execution for us!
我們切線程再切回來,有個關鍵字叫resume,用中文理解一下就是恢復狀態
而他怎麼恢復狀態呢? 他是透過coroutine的 Continuation ,來達到恢復狀態,這也是為甚麼suspend函數只能在coroutineScope或另一個suspend裡面執行,因為要用coroutine才能達到恢復狀態,對吧?
那Continuation 又是甚麼? 官方解釋說他是帶額外訊息的回調接口,source code長這樣
interface Continuation<in T> {
  public val context: CoroutineContext
  public fun resumeWith(value: Result<T>)
}
看這裡了解CoroutineContext
resumeWith,用Result回復coroutine的執行,可能包含執行結果或是Exception
那suspend編譯後長怎樣
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
  val user = userRemoteDataSource.logUserIn(userId, password)
  val userDb = userLocalDataSource.logUserIn(user)
  completion.resume(userDb)
}
這裡的completion非常重要,他是用來將suspend 的結果回傳給調用他的coroutine,但這只是簡化版的code
直接跟官方blog借code,我有附連結在下面,蠻建議去讀的,這邊我就簡單帶一下概念而已
suspend透過Continuation在不同suspend切換thread之間,傳遞value,並且透過轉型將Continuation轉換成 StateMachine 類別,利用label確定執行順序,如果是第一次執行fun,會建立State machine,之後每次都會將State Machine作為參數傳遞,遞迴呼叫loginUser function
直到最後,透過resume回傳了userDb,可以對應上面的code
/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
    class LoginUserStateMachine(
        // completion parameter is the callback to the function that called loginUser
        completion: Continuation<Any?>
    ): CoroutineImpl(completion) {
        // objects to store across the suspend function
        var user: User? = null
        var userDb: UserDb? = null
        // Common objects for all CoroutineImpl
        var result: Any? = null
        var label: Int = 0
        // this function calls the loginUser again to trigger the 
        // state machine (label will be already in the next state) and 
        // result will be the result of the previous state's computation
        override fun invokeSuspend(result: Any?) {
            this.result = result
            loginUser(null, null, this)
        }
    }
    val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
    when(continuation.label) {
        0 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Next time this continuation is called, it should go to state 1
            continuation.label = 1
            // The continuation object is passed to logUserIn to resume 
            // this state machine's execution when it finishes
            userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
        }
        1 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Gets the result of the previous state
            continuation.user = continuation.result as User
            // Next time this continuation is called, it should go to state 2
            continuation.label = 2
            // The continuation object is passed to logUserIn to resume 
            // this state machine's execution when it finishes
            userLocalDataSource.logUserIn(continuation.user, continuation)
        }
        2 -> {
            // Checks for failures
            throwOnFailure(continuation.result)
            // Gets the result of the previous state
            continuation.userDb = continuation.result as UserDb
            // Resumes the execution of the function that called this one
            continuation.cont.resume(continuation.userDb)
        }
        else -> throw IllegalStateException(...)
    }
}
大家都說,coroutine的掛起是非阻塞式的,真有那麼神奇的黑魔法嗎?
他的非阻塞式,是指不卡thread
在我們學coroutine的漫漫長路裡,一定會有文章說,coroutine是非阻塞式,thread是阻塞式,對,但也不對,因為他沒講明白,記得一點,kotlin 的coroutine是線程框架,它的本質是一樣的,thread的切換也是非阻塞式
那為什麼又說他對呢?
以單個thread來說,耗時任務是阻塞式的,那一單個coroutine來說呢,它可以是非阻塞式的,因為他能用suspend來切換線程,懂了嗎? coroutine是切換thread來達到非阻塞,那能不能用thread寫非阻塞代碼,當然可以,因為要做的事都一樣,就是切換thread
coroutine只是能用阻塞寫法寫出非阻塞代碼
以開發時間來看,有的
以程式執行來看,沒有
為甚麼呢,不是說掛起後thread就能執行其他任務嗎? 這樣不就不用痴痴等待
複習一下前面的概念,任務執行是cpu的工作,今天我們要執行io請求,要切到io thread對吧?
注意,一個耗時任務,是會慢慢執行,而不是在某個時間點突然完成,以前面範例來說,我們讓main thread掛起耗時任務去執行其他任務,為甚麼? 因為在main執行耗時任務ui會freeze,會ANR對吧?
那任務不做了嗎? 要做呀,只是我們拿到io thread執行了呀,要做的事情一件都沒有少
那suspend不是能掛起嗎? 那在io thread掛起,不就可以提升thread的利用率,這種想法很誘人,卻很誤導,也非常危險
前面我們講了什麼? 掛起的任務是對當前的thread來說,這個coroutine結束了,並且在suspend function執行完畢後,切回原本的thread,往裡面post之後的任務,那是不是還是要有thread去完成任務,畢竟他不會通靈,也不會自己完成呀
那能不能在一個thread裡面做併發呢?
io/ default可以有複數的thread,os會從thread pool拿出thread來執行任務,任務完成後要嘛回收要嘛再利用; 還有一點,async的併發會創造新的coroutine,是透過不同coroutine在不同 thread同時做多個任務,如果你的併發把耗時任務丟到Main thread的話,他是會照順序完成的。